<?php

class asynchronous_Object_Cronjob {

	/* a message is an action posted by another back end where some processing needs to be done. 
	The message gives the detail of which back end should be called and transfer parameters so the back end can perform the action requested
	*/
    protected $_id;
    
    //Store when the command should be executed
    //accepted formats: 
    //	* 			every
    //	*/u			every u units of time
    //	u			Specific unit of time
    //	u1,u2,u3	Specifics unit of time
    // 	u1-u2		Range between u1 and u2
    protected $_minute;
    protected $_hour;        
    protected $_day;
    protected $_month;   
    protected $_weekday; 
        
    //examples
	//    	30 	0 	1 	1,6,12 	* 	– 00:30 Hrs  on 1st of Jan, June & Dec.
	//		0 	20 	* 	10 	1-5 	–8.00 PM every weekday (Mon-Fri) only in Oct.
	//		0 	0 	1,10,15 	* 	* 	– midnight on 1st ,10th & 15th of month
	//		5,10 	0 	10 	* 	1 	– At 12.05,12.10 every Monday & on 10th of every month    
    
    //What should be executed
    protected $_module;
    protected $_controller;
    protected $_action;
    
        
	/* definition of the status table */
	static protected $_dbCronjobsTableName = 'cron_jobs';
	static protected $_dbCronjobsIdCol = 'id';
	static protected $_dbCronjobsDataCol = 'data';
	
	
	/* definition of the module controller action index */
	static protected $_dbCronjobsModuleIndexTableName = 'cron_jobModuleControllerActionIndex';
	static protected $_dbCronjobsModuleIndexJobIdCol = 'jobId';
	static protected $_dbCronjobsModuleIndexModuleNameCol = 'moduleName';
	static protected $_dbCronjobsModuleIndexControllerNameCol = 'controllerName';
	static protected $_dbCronjobsModuleIndexActionNameCol = 'actionName';
	
	
	/* timing formating */
	static protected $_dbCronjobsDataTimingContainer = 'timing';	
		static protected $_dbCronjobsDataMinute = 'minute';	
		static protected $_dbCronjobsDataHour = 'hour';	
		static protected $_dbCronjobsDataDay = 'day';	
		static protected $_dbCronjobsDataMonth = 'month';	
		static protected $_dbCronjobsDataWeekday = 'weekday';
		
	/* action formating */
	static protected $_dbCronjobsDataActionContainer = 'action';	
		static protected $_dbCronjobsDataModule = 'module';	
		static protected $_dbCronjobsDataController = 'controller';	
		static protected $_dbCronjobsDataAction = 'action';	

	
	//allowed unit time 
	static protected $_allowedUnitTime = array('minute','hour','day','month','weekday'); 
	//Range for each unit type
	static protected $_rangeUnitTime = array(
		'minute' => array('min' => 0, 'max' => 59),
		'hour' => array('min' => 0, 'max' => 23),
		'day' => array('min' => 1, 'max' => 31),
		'weekday' => array('min' => 1, 'max' => 7),
		'month' => array('min' => 1, 'max' => 12),
		
	);

	//limitation of timestamp for next run (in years), I think 5 years is reasonable for a cron job
	static protected $_limitNextRunYear = 5;

	
	
	public function __construct($id,$data = array()) {
		if(empty($data)) {
			//We validate that the id is numeric
			$validator = new Zend_Validate_Digits();
			if(!$validator->isValid($id)) {
				require_once('asynchronous/objects/Exception.php');
				throw new Asynchronous_Object_Exception('Invalid identifier',8); 	  	
			}
	  
			$db = self::_getDb();	
			$select = $db->select()->from(self::$_dbCronjobsTableName);
			$select->where($db->quoteInto(self::$_dbCronjobsIdCol ." = ?", $id));
			$res = $db->fetchAll($select->__toString());
			
			if(empty($res)) {
				require_once('asynchronous/objects/Exception.php');
				throw new Asynchronous_Object_Exception('cronjob not found',8); 
			}
			$data = $res[0];			
		}


		//we fill the object with the data
	    $this->_id = @$data[self::$_dbCronjobsIdCol];
	    
	    
	    //other fields are stored in the data object
		$decodedBlob = self::decodeJob($data[self::$_dbCronjobsDataCol]);	    
	    
	    
	    $this->_minute = $decodedBlob[self::$_dbCronjobsDataTimingContainer][self::$_dbCronjobsDataMinute];
	    $this->_hour = $decodedBlob[self::$_dbCronjobsDataTimingContainer][self::$_dbCronjobsDataHour];        
	    $this->_day = $decodedBlob[self::$_dbCronjobsDataTimingContainer][self::$_dbCronjobsDataDay];
	    $this->_month = $decodedBlob[self::$_dbCronjobsDataTimingContainer][self::$_dbCronjobsDataMonth];
	    $this->_weekday = $decodedBlob[self::$_dbCronjobsDataTimingContainer][self::$_dbCronjobsDataWeekday];	    
    	//What should be executed
    	$this->_module= $decodedBlob[self::$_dbCronjobsDataActionContainer][self::$_dbCronjobsDataModule];
    	$this->_controller = $decodedBlob[self::$_dbCronjobsDataActionContainer][self::$_dbCronjobsDataController];
    	$this->_action = $decodedBlob[self::$_dbCronjobsDataActionContainer][self::$_dbCronjobsDataAction];
		
		
		
	} //end __construct

	//return an array of asynchronous_Object_Cronjob objects
	public static function getAllJobs() {
			$db = self::_getDb();	
			$select = $db->select()->from(self::$_dbCronjobsTableName);
			$res = $db->fetchAll($select->__toString());	
			
			$output = array();
			if(empty($res)) return $output;
			
			foreach($res as $num => $data) {
				$output[] = new asynchronous_Object_Cronjob('',$data);
			}
			return $output;		
	}

	
	//Object model when returned in the API
	public function asArray() {
		$results = array();
	
		$results['id'] = $this->getId();
		$results['nextExecutionUnixTimestamp'] = $this->getNextExecutionTimestamp();
		$results['nextExecutionISO8601'] = date('c',$results['nextExecutionUnixTimestamp']);		
		
		$results['timing']['minute'] = $this->_minute;
		$results['timing']['hour'] = $this->_hour;
		$results['timing']['day'] = $this->_day;
		$results['timing']['month'] = $this->_month;
		$results['timing']['weekday'] = $this->_weekday;
		
		$results['action']['module'] = $this->_module;
		$results['action']['controller'] = $this->_controller;
		$results['action']['action'] = $this->_action;		
		
		//We get the last run
		require_once('asynchronous/objects/cronjobRun.php');
		$jobRun = asynchronous_Object_CronjobRun::getJobLastRun($this->getId());
		if(!empty($jobRun)) $results['lastRun'] = $jobRun->asArray();
		
		return $results;
	}


	public function getId() {
		return $this->_id;	
	}

	public function getModule() {
		return $this->_module;	
	}
	public function getController() {
		return $this->_controller;	
	}
	public function getAction() {
		return $this->_action;	
	}
	
	static public function add($params) {
		//we validate the params
		$dataTiming = array();
		//we loop through the 5 constituents of the cron
		foreach(self::$_allowedUnitTime as $unit) {
			$dataTiming[$unit] = (isset($params[$unit]))?$params[$unit]:'*';
			if(strlen($dataTiming[$unit]) == 0) $dataTiming[$unit] = '*';

			//bug where 0 is considered as empty (and isset doesnt work)
			$ok = self::_isValidUnit($dataTiming[$unit],$unit);
			if(!$ok) {
				require_once('asynchronous/objects/Exception.php');
				throw new Asynchronous_Object_Exception($unit.' is not valid, must be * or */n or n1-n2 or n1,n2,n3 with n between '.self::$_rangeUnitTime[$unit]['min'].' and '.self::$_rangeUnitTime[$unit]['max'],10); 
			}
		}
		
		if(empty($params['module']) || empty($params['controller']) || empty($params['action'])) {
			require_once('asynchronous/objects/Exception.php');
			throw new Asynchronous_Object_Exception('Module, controller or action is empty',3); 			
		}
		
		
		//We check that the cronjob is not already existing in the module controller action index
		$db = self::_getDb();	
		$select = $db->select()->from(self::$_dbCronjobsModuleIndexTableName);
		$select->where($db->quoteInto(self::$_dbCronjobsModuleIndexModuleNameCol ." = ?", $params['module']));
		$select->where($db->quoteInto(self::$_dbCronjobsModuleIndexControllerNameCol ." = ?", $params['controller']));
		$select->where($db->quoteInto(self::$_dbCronjobsModuleIndexActionNameCol ." = ?", $params['action']));
		$res = $db->fetchAll($select->__toString());
		
		if(count($res) > 0) {
			require_once('asynchronous/objects/Exception.php');
			throw new Asynchronous_Object_Exception('Cronjob is already existing',4); 			
		}
		
		//we update the database with the new job
		$data = array();
		/* timing formating */
		$data[self::$_dbCronjobsDataTimingContainer][self::$_dbCronjobsDataMinute] = $dataTiming['minute'];	
		$data[self::$_dbCronjobsDataTimingContainer][self::$_dbCronjobsDataHour] = $dataTiming['hour'];	
		$data[self::$_dbCronjobsDataTimingContainer][self::$_dbCronjobsDataDay] = $dataTiming['day'];	
		$data[self::$_dbCronjobsDataTimingContainer][self::$_dbCronjobsDataWeekday] = $dataTiming['weekday'];
		$data[self::$_dbCronjobsDataTimingContainer][self::$_dbCronjobsDataMonth] = $dataTiming['month'];

			
		/* action formating */

		
		$data[self::$_dbCronjobsDataActionContainer][self::$_dbCronjobsDataModule] = $params['module'];	
		$data[self::$_dbCronjobsDataActionContainer][self::$_dbCronjobsDataController] = $params['controller'];	
		$data[self::$_dbCronjobsDataActionContainer][self::$_dbCronjobsDataAction] = $params['action'];	


		$toBesaved = array(self::$_dbCronjobsDataCol => self::encodeJob($data));
	  	
	  	//We check that the cronjob is possible (avoid things like 30 february)
	  	$cronJob = new asynchronous_Object_Cronjob('',$toBesaved);
	  	$cronJob->getNextExecutionTimestamp();
	  	
	  	
	  	//and we put it in the database
	    $db->insert(self::$_dbCronjobsTableName, $toBesaved);

	    //we get the last inserted Id
	    $jobId = $db->lastInsertId();
	    	    
	    
	    //We update the module controller action index
	    $indexData = array(
	    	self::$_dbCronjobsModuleIndexJobIdCol => $jobId,
	    	self::$_dbCronjobsModuleIndexModuleNameCol => $params['module'],
	    	self::$_dbCronjobsModuleIndexControllerNameCol => $params['controller'],
	    	self::$_dbCronjobsModuleIndexActionNameCol => $params['action']
	    );
	    $db->insert(self::$_dbCronjobsModuleIndexTableName, $indexData);	    

		
		return $jobId;
		
	} //end add

	
	//to do, provide a way to retrieve the jobs based on module controller action (external index)
	protected static function _isCronJobAlreadyExists($module,$controller,$action) {
	
	}
	
	/* publishJobsToRun - retrieve the list of jobs that are not planned in the jobrun queue and publish them
		@param - empty
		@return array JobId => runId
	*/
	public static function publishJobsToRun() {
		require_once('asynchronous/objects/cronjobRun.php');
		
		
		//we get the list of published jobs
Zend_Registry::get('dt_log')->log('Retrieving list of published jobs not run yet',Zend_Log::DEBUG);
		$publishedJobs = asynchronous_Object_CronjobRun::getPublishedJobNotRunned();
		$nbPublishedJobs = count($publishedJobs);
Zend_Registry::get('dt_log')->log($nbPublishedJobs.' published jobs found',Zend_Log::DEBUG);
		
		
		
		//We get the list of all jobs
Zend_Registry::get('dt_log')->log('Retrieving list of all jobs',Zend_Log::DEBUG);		
		$allJobs = self::_getAllJobsId();
		$nbJobs = count($allJobs);
Zend_Registry::get('dt_log')->log($nbJobs.' jobs found',Zend_Log::DEBUG);		
		
	//If no job defined, no job to publish
	if(empty($allJobs)) return array();
	//If same amount of jobs published than jobs, nothing to publish
	if($nbPublishedJobs == $nbJobs) return array();


		//the substraction gives us the list of jobs that requires publishing
		$toBePublishedJobs = array_diff($allJobs, $publishedJobs);	
Zend_Registry::get('dt_log')->log(count($toBePublishedJobs).' jobs to be published',Zend_Log::DEBUG);	
		//We tell cronjobRun to publish them
		$runIdList = array();
		foreach($toBePublishedJobs as $num => $jobId) {
			try{
				$job = new asynchronous_Object_Cronjob($jobId);
				$runIdList[$jobId] = asynchronous_Object_CronjobRun::publishJob($job);
			}	catch(Asynchronous_Object_Exception $e) {
Zend_Registry::get('dt_log')->log('Issue occured whil publishing job '.$jobId.': '.$e->getMessage(),Zend_Log::ERR);				
			}
		}
		return $runIdList;
		
		
	} // end publishJobsToRun
	
	
	protected static function _getAllJobsId() {
			$db = self::_getDb();	
			$select = $db->select()->from(self::$_dbCronjobsTableName,self::$_dbCronjobsIdCol);
			$res = $db->fetchAll($select->__toString());	
			
			$output = array();
			if(empty($res)) return $output;
			
			foreach($res as $num => $data) {
				$output[] = $data[self::$_dbCronjobsIdCol];
			}
			return $output;
	}
	
	//return the next time the job will be executed
	public function getNextExecutionTimestamp() {
		//we need the current time to make our calculation		
		require_once('asynchronous/objects/Date.php');
		$next = new asynchronous_Object_Date(time());
		
		//some check to avoid infinite loop
		$yearLimit = $next->getYear() + self::$_limitNextRunYear;
		
		$next->modify('+1 minute');
	//http://stackoverflow.com/questions/321494/calculate-when-a-cron-job-will-be-executed-then-next-time

		while (true) {
		  //we limit the search to avoid fake date like 30th of February
		  if($yearLimit < $next->getYear()) {
				require_once('asynchronous/objects/Exception.php');
				throw new Asynchronous_Object_Exception('Next execution Timestamp is too far in the future',2); 		  		
		  }
		  
		  if (!self::_isMatching('month',$next->getMonth())) {
		    $next->modify('+1 month');
		    $next->reset('day');
		    continue;
		  }
		  if (!self::_isMatching('day', $next->getDay())) {
		    $next->modify('+1 day');
		    $next->reset('hour');
		    continue;
		  }
		  if (!self::_isMatching('weekday', $next->getWeekday())) {
		    $next->modify('+1 day');
		    $next->reset('hour');
		    continue;
		  }
		  if (!self::_isMatching('hour',$next->getHour())) {
		    $next->modify('+1 hour');
		    $next->reset('minute');
		    continue;
		  }
		  if (!self::_isMatching('minute',$next->getMinute())) {
		    $next->modify('+1 minute');
		    continue;
		  }
		  break;
		}
		return $next->getTimestamp();

	}
	
	
	protected function _isMatching($unit,$unitCurrentValue) {
		switch ($unit) {
		    case 'minute':
		        $value = $this->_minute;
		        break;
		    case 'hour':
		        $value = $this->_hour;
		        break;;
		    case 'day':
		        $value = $this->_day;
		        break;
		    case 'weekday':
		        $value = $this->_weekday;
		        break;
			case 'month':
		        $value = $this->_month;  
		        break;
		}
		//matching *
		if($value == '*') return true;
		//matching a particular value
		if($value == $unitCurrentValue) return true;
		//matching a range of value
		if(preg_match('/^(\d{1,2})-(\d{1,2})$/',$value,$matches)) {
			$min = $matches[1];
			$max = $matches[2];
			if($unitCurrentValue >= $min && $unitCurrentValue < $max) return true;	
		}
		//matching a modulo of value
		if(preg_match('/^\*\/(\d{1,2}$)/',$value,$matches)) {
			$modulo = $matches[1];
			if($unitCurrentValue%$modulo == 0) return true;
		}

		return false;
		        	
	}


	static protected function _isValidUnit($value,$unit) {
		//matching *
		if($value == '*') return true;
		//matching a particular value
		if(preg_match('/^(\d{1,2})$/',$value)) {
Zend_Registry::get('dt_log')->log($unit.' matched a particular value '.$value,Zend_Log::DEBUG);
			//We have to check if the value is in the range of accepted value
			if($value < self::$_rangeUnitTime[$unit]['min'] || $value > self::$_rangeUnitTime[$unit]['max']) return false;
			return true;
		}
		//matching a range of value
		if(preg_match('/^(\d{1,2})-(\d{1,2})$/',$value,$matches)) {
			$min = $matches[1];
			$max = $matches[2];
Zend_Registry::get('dt_log')->log($unit.' matched a range of values '.$min.'-'.$max,Zend_Log::DEBUG);
			if($min >= $max) return false;
			if($min < self::$_rangeUnitTime[$unit]['min']) return false;
			if($max > self::$_rangeUnitTime[$unit]['max']) return false;
			return true;
		}
		//matching a modulo of value
		if(preg_match('/^\*\/(\d{1,2})$/',$value,$matches)) {

			$modulo = $matches[1];

			Zend_Registry::get('dt_log')->log($unit.' matched a modulo '.$modulo,Zend_Log::DEBUG);

			if($modulo < self::$_rangeUnitTime[$unit]['min'] || $modulo > self::$_rangeUnitTime[$unit]['max']) return false;
			return true;
		}
		return false;
	}
	


	//the message is stored as a base64 gzdefalted serialized PHP
	static public function decodeJob($data) {
		$decoded = @unserialize(gzinflate(base64_decode($data)));
		if(empty($decoded)) {
			require_once('asynchronous/objects/Exception.php');
			throw new Asynchronous_Object_Exception('An issue occured while decoding message',16);
		}  
		return  $decoded;	
	}

	static public function encodeJob($data) {
	  	if(!is_array($data)) {
	  		require_once('asynchronous/objects/Exception.php');
	  		throw new Asynchronous_Object_Exception('input parameter is not an array',12); 
	  	}
	    $encoded = @base64_encode(gzdeflate(serialize($data),9));
	    if(empty($encoded)) {
	    	require_once('asynchronous/objects/Exception.php');
	    	throw new Asynchronous_Object_Exception('An issue occured while encoding message',17);
	    } 
	    return $encoded;   	
	}	
	
	static protected function _getDb() {
	  	return Zend_Registry::get('dt_dbs')->getDb('asynchronous','cronjob','key');	
	}
  
} //end class